/* Copyright (c) 2008, The Visigoth Software Society
 * Copyright (c) 2006, Sun Microsystems Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notices,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name Visigoth Software Society, FreeCC, Sun Microsystems, Inc. 
 *       nor the names of any other contributors may be used to endorse 
 *       or promote products derived from this software without specific prior written 
 *       permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.visigoths.freecc;

import java.io.*;
import java.util.*;

import org.visigoths.freecc.codegen.java.*;
import org.visigoths.freecc.lexgen.*;
import org.visigoths.freecc.output.java.FilesGenerator;
import org.visigoths.freecc.parsegen.*;
import org.visigoths.freecc.parser.*;
import freemarker.template.TemplateException;

/**
 * This object is basically the root object of a class hierarchy that maintains
 * all the information regarding a JavaCC processing job.
 */
public class Grammar {

    private String filename, parserClassName;
    private ClassOrInterfaceBody tokenMgrDecls;
    private CompilationUnit parserCode;
    private FreeCCOptions options = new FreeCCOptions(this);
    private Action eofAction;
    private String nextStateForEOF;
    private Semanticizer semanticizer;
    private FilesGenerator filesGenerator;
    private long nextGenerationIndex = 1L;
    private int lookaheadLimit;
    private boolean considerSemanticLA;
    private List<MatchInfo> sizeLimitedMatches;
    private boolean unicodeWarningGiven;
    private List<String> jjtThisStack = new ArrayList<String>();
    private ParserData parserData;
    private LexerData lexerData = new LexerData(this);
    private int includeNesting;

    private List<TokenProduction> rexprlist = new ArrayList<TokenProduction>();
    private List<NormalProduction> bnfProductions = new ArrayList<NormalProduction>();
    private Map<String, NormalProduction> productionTable = new HashMap<String, NormalProduction>();
    private Map<String, RegularExpression> namedTokensTable = new LinkedHashMap<String, RegularExpression>();
    private List<FreeCCError> errors = new ArrayList<FreeCCError>();
    private CodeInjections codeInjections;
    private Map<Integer, RegularExpression> regexpLookup = new HashMap<Integer, RegularExpression>();
    private Map<Integer, String> tokenNames = new HashMap<Integer, String>();
    private Set<String> nodeIds = new LinkedHashSet<String>();
    private Set<String> nodeNames = new LinkedHashSet<String>();
    private Set<String> lexicalStates = new LinkedHashSet<String>();

    public Grammar() {
        addLexicalState("DEFAULT");
    }
    
    public Grammar(FreeCCOptions options) {
        this();
        this.options = options;
        options.setGrammar(this);
    }
    
    void parse(String location) throws IOException, ParseException {
        Reader input = new FileReader(location);
        this.setFilename(location);
        FreeCCParser parser = new FreeCCParser(this, input);
        parser.setInputSource(location);
        setFilename(location);
        System.out.println("Parsing grammar file " + location + " . . .");
        parser.Root();
    }
    
    public void include(String location) throws IOException, ParseException {
        File file = new File(location);
        if (!file.exists()) {
            if (!file.isAbsolute()) {
                file = new File(new File(this.filename).getParent(), location);
                if (file.exists()) {
                    location = file.getAbsolutePath();
                }
            }
        }
        includeNesting++;
        parse(location);
        includeNesting--;
    }
    
    
    public void createOutputDir() {
        File outputDir = getOptions().getOutputDirectory();
        if (!outputDir.exists()) {
            addWarning(null, "Output directory \"" + outputDir + "\" does not exist. Creating the directory.");

            if (!outputDir.mkdirs()) {

                addSemanticError(null, "Cannot create the output directory : " + outputDir);
                return;
            }
        }

        else if (!outputDir.isDirectory()) {
            addSemanticError(null, "\"" + outputDir + " is not a valid output directory.");
        }

        else if (!outputDir.canWrite()) {
            addSemanticError(null, "Cannot write to the output output directory : \"" + outputDir + "\"");
        }
    }

    /**
     * The name of the grammar file being processed.
     */
    public String getFilename() {
        return filename;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public void generateLexer() {
        lexerData.start();
    }

    public void semanticize() throws MetaParseException {
        for (String lexicalState : lexicalStates) {
            lexerData.addLexicalState(lexicalState);
        }
        semanticizer = new Semanticizer(this);
        semanticizer.start();
    }

    public void buildParserInfo() throws MetaParseException {
        parserData = new ParserData(this);
        parserData.start();
    }

    public void generateFiles() throws ParseException, IOException, TemplateException {
        filesGenerator = new FilesGenerator(this, codeInjections);
        filesGenerator.generateAll();
    }

    public LexerData getLexerData() {
        return lexerData;
    }

    public ParserData getParserData() {
        return parserData;
    }

    public FreeCCOptions getOptions() {
        return options;
    }
    
    void setOptions(FreeCCOptions options) {
        this.options = options;
        options.setGrammar(this);
    }

    /**
     * The name of the parser class (what appears in PARSER_BEGIN and
     * PARSER_END).
     */
    public String getParserClassName() {
        return parserClassName;
    }

    public CompilationUnit getParserCode() {
        return parserCode;
    }

    public TypeDeclaration getParserClass() {
        for (int i = 0; i < parserCode.jjtGetNumChildren(); i++) {
            Node child = parserCode.jjtGetChild(i);
            if (child instanceof TypeDeclaration) {
                TypeDeclaration jcd = (TypeDeclaration) child;
                if (jcd.getName().equals(parserClassName)) {
                    return jcd;
                }
            }
        }
        return null;
    }

    public List<Node> getOtherParserCodeDeclarations() {
        List<Node> result = new ArrayList<Node>();
        for (int i = 0; i < parserCode.jjtGetNumChildren(); i++) {
            Node child = parserCode.jjtGetChild(i);
            if (child instanceof Token) {
                continue;
            }
            if (child instanceof TypeDeclaration) {
                if (((TypeDeclaration) child).getName().equals(parserClassName)) {
                    continue;
                }
            }
            result.add(child);
        }
        return result;
    }

    public void setParserCode(CompilationUnit parserCode) {
        this.parserCode = parserCode;
        codeInjections = new CodeInjections(parserCode.getPackageName());
    }

    public void setParserClassName(String parserClassName) {
        this.parserClassName = parserClassName;
    }

    /**
     * A list of all grammar productions - normal and JAVACODE - in the order
     * they appear in the input file. Each entry here will be a subclass of
     * "NormalProduction".
     */
    public List<NormalProduction> getBNFProductions() {
        return bnfProductions;
    }

    public void addBNFProduction(NormalProduction production) {
        bnfProductions.add(production);
    }

    /**
     * A symbol table of all grammar productions - normal and JAVACODE. The
     * symbol table is indexed by the name of the left hand side non-terminal.
     * Its contents are of type "NormalProduction".
     */
    public Map<String, NormalProduction> getProductionTable() {
        return productionTable;
    }

    public NormalProduction getProductionByLHSName(String name) {
        return productionTable.get(name);
    }

    /**
     * Add a new lexical state
     */
    public void addLexicalState(String name) {
        lexicalStates.add(name);
        // if (lexerData.getLexicalState(name) == null)
        // lexerData.addLexicalState(name);
    }

    /**
     * The list of all TokenProductions from the input file. This list includes
     * implicit TokenProductions that are created for uses of regular
     * expressions within BNF productions.
     */
    public List<TokenProduction> getAllTokenProductions() {
        return rexprlist;
    }

    public void addTokenProduction(TokenProduction tp) {
        rexprlist.add(tp);
    }

    public RegularExpression getRegexpForToken(int index) {
        return regexpLookup.get(index);
    }

    public void addRegularExpression(int index, RegularExpression regexp) {
        regexpLookup.put(index, regexp);
    }

    /**
     * This is a symbol table that contains all named tokens (those that are
     * defined with a label). The index to the table is the image of the label
     * and the contents of the table are of type "RegularExpression".
     */
    public RegularExpression getNamedToken(String name) {
        return namedTokensTable.get(name);
    }

    public RegularExpression addNamedToken(String name, RegularExpression regexp) {
        return namedTokensTable.put(name, regexp);
    }

    /**
     * Contains the same entries as "named_tokens_table", but this is an ordered
     * list which is ordered by the order of appearance in the input file.
     * (Actually, the only place where this is used is in generating the
     * XXXConstants.java file)
     */
    public List<RegularExpression> getOrderedNamedTokens() {
        List<RegularExpression> result = new ArrayList<RegularExpression>();
        result.addAll(namedTokensTable.values());
        return result;
    }

    /**
     * A mapping of ordinal values (represented as objects of type "Integer") to
     * the corresponding labels (of type "String"). An entry exists for an
     * ordinal value only if there is a labeled token corresponding to this
     * entry. If there are multiple labels representing the same ordinal value,
     * then only one label is stored.
     */

    public String getTokenName(int index) {
        String tokenName = tokenNames.get(index);
        return tokenName == null ? String.valueOf(index) : tokenName;
    }

    public void addTokenName(int index, String name) {
        tokenNames.put(index, name);
    }

    public Action getEofAction() {
        return eofAction;
    }

    public void setEofAction(Action eofAction) {
        this.eofAction = eofAction;
    }

    public String getNextStateForEOF() {
        return this.nextStateForEOF;
    }

    public void setNextStateForEOF(String nextStateForEOF) {
        this.nextStateForEOF = nextStateForEOF;
    }

    public int getErrorCount() {
        int result = 0;
        for (FreeCCError error : errors) {
            if (error.type != FreeCCError.Type.WARNING)
                ++result;
        }
        return result;
    }

    public int getWarningCount() {
        int result = 0;
        for (FreeCCError error : errors) {
            if (error.type == FreeCCError.Type.WARNING)
                ++result;
        }
        return result;
    }

    public int getParseErrorCount() {
        int result = 0;
        for (FreeCCError error : errors) {
            if (error.type == FreeCCError.Type.PARSE)
                ++result;
        }
        return result;
    }

    public int getSemanticErrorCount() {
        int result = 0;
        for (FreeCCError error : errors) {
            if (error.type == FreeCCError.Type.SEMANTIC)
                ++result;
        }
        return result;
    }

    public void addSemanticError(FreeCCNode node, String message) {
        FreeCCError error = new FreeCCError(FreeCCError.Type.SEMANTIC, message, node);
        System.err.println(error);
        errors.add(error);
    }

    public void addParseError(FreeCCNode node, String message) {
        FreeCCError error = new FreeCCError(FreeCCError.Type.PARSE, message, node);
        System.err.println(error);
        errors.add(error);
    }

    public void addWarning(FreeCCNode node, String message) {
        FreeCCError error = new FreeCCError(FreeCCError.Type.WARNING, message, node);
        System.err.println(error);
        errors.add(error);
    }

    /**
     * To avoid right-recursive loops when calculating follow sets, we use a
     * generation number which indicates if this expansion was visited by
     * LookaheadWalk.genFollowSet in the same generation. New generations are
     * obtained by incrementing the counter below, and the current generation is
     * stored in the non-static variable below.
     */
    public long nextGenerationIndex() {
        return nextGenerationIndex++;
    }

    public int getLookaheadLimit() {
        return lookaheadLimit;
    }

    public void setLookaheadLimit(int lookaheadLimit) {
        this.lookaheadLimit = lookaheadLimit;
    }

    public boolean considerSemanticLA() {
        return considerSemanticLA;
    }

    public void setConsiderSemanticLA(boolean b) {
        considerSemanticLA = b;
    }

    public List<MatchInfo> getSizeLimitedMatches() {
        return sizeLimitedMatches;
    }

    public void setSizeLimitedMatches(List<MatchInfo> sizeLimitedMatches) {
        this.sizeLimitedMatches = sizeLimitedMatches;
    }

    public boolean wasUnicodeWarningGiven() {
        return unicodeWarningGiven;
    }

    public void setUnicodeWarningGiven(boolean b) {
        this.unicodeWarningGiven = b;
    }

    public boolean isJdk15() {
        return options.jdkVersionAtLeast(1.5);
    }

    public String getJjtThis() {
        if (jjtThisStack.isEmpty())
            return "null";
        return jjtThisStack.get(jjtThisStack.size() - 1);
    }

    public void pushJjtThis(String jjtThis) {
        jjtThisStack.add(jjtThis);
    }

    public void popJjtThis() {
        jjtThisStack.remove(jjtThisStack.size() - 1);
    }

    public Set<String> getNodeIds() {
        return nodeIds;
    }

    public Set<String> getNodeNames() {
        return nodeNames;
    }

    public void addNodeType(String nodeName) {
        if (nodeName.equals("void")) {
            return;
        }
        String id = "JJT" + nodeName.toUpperCase();
        nodeIds.add(id);
        nodeNames.add(nodeName);
    }

    public void setTokenMgrDecls(ClassOrInterfaceBody tokenMgrDecls) {
        this.tokenMgrDecls = tokenMgrDecls;
        for (Node n : tokenMgrDecls) {
            if (matchesTokenAction(n)) {
                options.setCommonTokenAction(true);
            }
        }
    }

    // A bit kludgy

    private boolean matchesTokenAction(Node node) {
        if (node instanceof ClassOrInterfaceBodyDeclaration) {
            ClassOrInterfaceBodyDeclaration decl = (ClassOrInterfaceBodyDeclaration) node;
            String sig = decl.getFullNameSignatureIfMethod();
            if (sig != null) {
                StringTokenizer st = new StringTokenizer(sig);
                if (st.hasMoreTokens()) {
                    if (st.nextToken().equals("CommonTokenAction")) {
                        if (st.hasMoreTokens()) {
                            if (st.nextToken().equals("(")) {
                                return true;
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    public Node getTokenManagerDeclarations() {
        return tokenMgrDecls;
    }

    public void injectCode(CompilationUnit jcu) {
        codeInjections.add(jcu);
    }

    public void injectCode(String name, ClassOrInterfaceBody body, boolean isInterface) {
        codeInjections.add(name, body, isInterface);
    }
    
    public boolean isInInclude() {
        return includeNesting >0;
    }
    
    String getParserPackageName() {
        if (parserCode != null ) {
            return parserCode.getPackageName();
        }
        return null;
    }
    
    public File getParserOutputDirectory() throws IOException {
        String baseSrcDir = options.getBaseSourceDirectory();
        if (baseSrcDir.equals("")) {
            return options.getOutputDirectory();
        }
        File dir = new File(baseSrcDir);
        if (!dir.isAbsolute()){
            File inputFileDir = new File(filename).getParentFile();
            dir = new File(inputFileDir, baseSrcDir);
        }
        if (!dir.exists()) {
            throw new FileNotFoundException("Directory " + dir + " does not exist.");
        }
        String packageName = getParserPackageName();
        if (packageName != null) {
            packageName = packageName.replace('.', '/');
            dir = new File(dir, packageName);
            if (!dir.exists()) {
                dir.mkdir();
            }
        }
        return dir;
    }
}
